前幾篇多多少少都有提到 ViewModel,今天終於要來講講 ViewModel 的故事,以下如有解釋不清或是描述錯誤的地方還請大家多多指教:
用來處理給介面呈現的資料邏輯,ViewModel 本身也有自己的生命周期,在前幾篇的 lifecycle 中也有小提到,透過 LiveData 傳遞給介面所需資訊, ViewModel 不可持有 View 的 context,不然 View 在銷毀時無法完全的死亡,property 有可能無法被 GC 而造成 memory leak,而 Architecture Components 提供了 ViewModel class 來協助我們實作 ViewModel。
ViewModel 會持有資料的 memory 直到持有他 instance 的頁面生命週期結束,以頁面來說:
我們透過 delegate 的方式來建立我們的 ViewModel,這邊分成兩種方式:
// 一個 instance
private val viewModel by activityViewModels<MyViewModelName>()
// 當個 fragment 建立一個新的 instance
private val viewModel by viewModels<MyViewModelName>()
這兩個有什麼差別呢? activityViewModels 只會在 activity 建立出一個 ViewModel,所有透過 activityViewModels 產出的 fragment 都會吃到從這個 viewModel 送出來的值,例如:
class MainActivity: AppCompatActivity() {
private val viewModel by viewModels<MyViewModelName>()
...
fun setupView() {
viewModel.getCity()
}
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// catch data
}
}
}
class HomeFragment : Fragment() {
private val viewModel by activityViewModels<MyViewModelName>()
private val listAdapter by lazy { MyAdapterName() }
...
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// catch data
}
}
}
class DetailFragment : Fragment() {
private val viewModel by activityViewModels<MyViewModelName>()
private val listAdapter by lazy { MyAdapterName() }
...
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// catch data
}
}
}
如果每個 fragment 都是透過 by viewModels 來產,那每個 view 透過 viewModel 執行的事情都不會干擾到另一個 view,也就是說每個 view 都建立一個新的 viewModel:
class MainActivity: AppCompatActivity() {
private val viewModel by viewModels<MyViewModelName>()
...
fun setupView() {
viewModel.getCity()
}
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// catch data
}
}
}
class HomeFragment : Fragment() {
private val viewModel by viewModels<MyViewModelName>()
private val listAdapter by lazy { MyAdapterName() }
...
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// won't catch data
}
}
}
class DetailFragment : Fragment() {
private val viewModel by viewModels<MyViewModelName>()
private val listAdapter by lazy { MyAdapterName() }
...
fun setupViewModel() {
viewModel.cityList.observe(viewLifecycleOwner) {
// won't catch data
}
}
}
我們在 lifecycle 那篇已經有加過 ViewModel 的 dependency 了,而 ViewModel 透過 delegate by
來建立 是 androidx.fragment:fragment-ktx
所提供方法:
建立一個 MainViewModel 並繼承 ViewModel()
class MainViewModel: ViewModel() {
}
在 main 使用 viewModels delegate 來幫我們取得要使用的 ViewModel:
private val viewModel by viewModels<MainViewModel>()
在 home 使用 activityViewModels delegate 來幫我們取得 activity 使用的 ViewModel:
private val viewModel by activityViewModels<MainViewModel>()
將先前寫在 main 的 API 邏輯搬移到 ViewModel 去,並使用 viewModelScope 去呼叫 API 及 DB ,viewModelScope 會在 ViewModel 在 onCleared 時取消所有已啟動的工作:
class MainViewModel: ViewModel() {
fun getForecast(country: String) {
val service = WeathbyRetrofit.makeRetrofitService()
viewModelScope.launch {
runCatching {
service.getForecast(query = country)
}.onSuccess {
Log.i("success", "onCreate: $it")
}.onFailure {
Log.i("fail", "onCreate: $it")
}
}
}
}
// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.getForecast()
}
點擊 viewModelScope 可以看到 job 是設定 Main,以及 close 會 cancel 所有工作:
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
/**
* [CoroutineScope] tied to this [ViewModel].
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}